Entendimiento del negocio (CRISP-DM)¶
Incluye: Explicación de la problemática, preguntas de negocio que se quieren responder, posibles riesgos, las limitaciones existentes y los criterios de éxito.
Usamos como guía "https://pdf4pro.com/amp/download?data_id=73581e&slug=crisp-dm-1-the-modeling-agency-com".
Repositorio del dataset: "https://archive.ics.uci.edu/dataset/890/aids+clinical+trials+group+study+175"
En el entendimiento del negocio, según nuestra guía, se realizan las siguientes tareas:
- Determinar los objetivos del negocio
- Evaluar el panorama
- Determinar las metas de la minería de datos
- Producir un plan del proyecto
Evaluar el panorama¶
El conjunto de datos que se nos proporciona lleva por nombre “Conjunto de datos del Estudio 175 del Grupo de Ensayos Clínicos sobre el SIDA”. Esta recopilación fue financiada por el Instituto Nacional de Alergias y Enfermedades Infecciosas, así como por las Unidades del Centro General de Investigación, respaldadas por el Centro Nacional de Recursos para la Investigación, ambos organismos de los Estados Unidos.
Los datos representan historiales clínicos de pacientes mayores de 12 años, estadounidenses con SIDA. Los pacientes fueron seleccionados de manera aleatoria, exceptuando aquellos que poseían algunas de las condiciones siguientes: alergias al ddc, ddl o AZT; neuropatía periférica de grado 2 o mayor; pancreatitis aguda o crónica; terapia aguda para una infección o enfermedad en los últios 14 días; reciente abuso de alcohol. Para este estudio, se asignó a cada paciente un tratamiento al azar de AZT, AZT y ddl, AZT y ddc, o ddl; esto se realizó durante al menos dos años
El SIDA es causado por el VIH (Virus de la Inmunodeficiencia Humana), este no tiene cura. El VIH fue identificado en 1983 (https://www.nobelprize.org/prizes/medicine/2008/barre-sinoussi/facts/), mientras que el dataset fue publicado, por primera vez, 13 años después (1996).
Determinar los objetivos del negocio¶
Conociéndo la fuente de los datos, así como los organismos que financiaron este estudio, los objetivos del negocio serán los siguientes:
- Describir la población afectada.
- Determinar la eficacia y seguridad de distintos tratamientos, para encontrar el que mejor prevenga la progresión del VIH
- Predecir qué pacientes morirán en cierto intervalo de tiempo.
Preguntas de negocio.¶
- ¿Cómo se puede describir la población afectada por VIH y posteriormente SIDA?
- ¿Cual es la eficacia de cada tratamiento suministrado, y cual provee una mejor prevención al progreso del VIH?
- ¿Qué pacientes son propensos a morir en un intervalo de tiempo determinado?
Éxito del negocio¶
El éxito de negocio se da en el caso de que se pueda determinar y predecir correctamente la muerte de pacientes, y tomar la decisión correcta respecto a la seguridad y eficacia de los tratamientos usados en los pacientes que padecen de VIH o SIDA.
Riesgos del negocio¶
Existen algunos riesgos para llevar a cabo este negocio, estos son: mal uso de tratamiento de datos personales del paciente e información sensible (como la raza, orientación sexual, etc); que el tratamiento pueda causar algún efecto secundario a un paciente, de manera que afecte significativamente y de manera negativa al paciente; que el paciente decida retirarse del tratamiento por motivos personales, antes del tiempo determinado.
Limitaciones del negocio¶
Una limitación posible del negocio es la confiabilidad de los datos por la época en los cuales fueron registrados. Es decir, posibles respuestas erróneamente diligenciadas al poder contener información sensible. Otra limitación es la restricción geográfica y demográfica, al ser todos los pacientes Americanos, lo que limita la variabilidad socioeconómica, étnica y geográfica.
Determinar las metas de la minería de datos¶
El propósito del dataset, según su repositorio, fue recopilar información útil para determinar los rendimientos de tipos de tratamiento del VIH. Además, servir para entrenar modelos predictivos que puedan determinar si un paciente morirá cierto tiempo después
Para esto se recolectaron 23 características, excluímos algunas de estas según las instrucciones del taller.
Producir un plan del proyecto¶
El plan inicial es el siguiente:
- Crear un repositorio en Github para sincronizar labores del equipo.
- Crear un diccionario de datos para entender con qué se trabaja.
- Una vez cargados los datos, realizar un análisis exploratorio con el objetivo de clasificar su calidad.
- Verificar si los datos son adecuados para los objetivos del negocio. En caso contrario, replantear estos últimos.
- Caracterizar la población incluída en el dataset, hacer esto visualmente. (Primer objetivo)
- Consultar los tratamientos presentes en el conjunto de datos a fin de entender qué significa esta variable.
- Describir numérica y visualmente la efectividad de los distintos tratamientos usados en la población estudiada. (Segundo objetivo)
- Desarrollar un modelo predictivo que permita determinar, en cierto intervalo de tiempo, qué pacientes morirán. (Tercer objetivo)
- Explorar distintos modelos predictivos y, según las capacidades técnicas y recursos disponibles, seleccionar el más adecuado para el proyecto.
Este plan debe considerarse un borrador inicial. Será ajustado conforme se evalúe la efectividad de la metodología aplicada.
Carga de datos¶
Excluímos, según las instrucciones, las variables: oprior, z30, zprior, preanti, str2, strat, offtrt, cd40, cd420, cd80, cd820.
import pandas as pd
urlcsv = "http://archive.ics.uci.edu/static/public/890/data.csv"
dfs = pd.read_csv(urlcsv)
no_data = ["oprior", "z30", "zprior", "preanti", "str2", "strat", "offtrt", "cd40", "cd420", "cd80", "cd820"]
df = dfs.drop(no_data, axis=1)
df
| pidnum | cid | time | trt | age | wtkg | hemo | homo | drugs | karnof | race | gender | symptom | treat | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10056 | 0 | 948 | 2 | 48 | 89.8128 | 0 | 0 | 0 | 100 | 0 | 0 | 0 | 1 |
| 1 | 10059 | 1 | 1002 | 3 | 61 | 49.4424 | 0 | 0 | 0 | 90 | 0 | 0 | 0 | 1 |
| 2 | 10089 | 0 | 961 | 3 | 45 | 88.4520 | 0 | 1 | 1 | 90 | 0 | 1 | 0 | 1 |
| 3 | 10093 | 0 | 1166 | 3 | 47 | 85.2768 | 0 | 1 | 0 | 100 | 0 | 1 | 0 | 1 |
| 4 | 10124 | 0 | 1090 | 0 | 43 | 66.6792 | 0 | 1 | 0 | 100 | 0 | 1 | 0 | 0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2134 | 990021 | 0 | 1091 | 3 | 21 | 53.2980 | 1 | 0 | 0 | 100 | 0 | 1 | 0 | 1 |
| 2135 | 990026 | 0 | 395 | 0 | 17 | 102.9672 | 1 | 0 | 0 | 100 | 1 | 1 | 0 | 0 |
| 2136 | 990030 | 0 | 1104 | 2 | 53 | 69.8544 | 1 | 1 | 0 | 90 | 1 | 1 | 0 | 1 |
| 2137 | 990071 | 1 | 465 | 0 | 14 | 60.0000 | 1 | 0 | 0 | 100 | 0 | 1 | 0 | 0 |
| 2138 | 990077 | 0 | 1045 | 3 | 45 | 77.3000 | 1 | 0 | 0 | 100 | 0 | 1 | 0 | 1 |
2139 rows × 14 columns
Diccionario de datos¶
Descripción de la tabla, volumetría, descripción y tipo de cada columna, etc
"DiccionarioDeDatos.xlsx"
df.head()
| pidnum | cid | time | trt | age | wtkg | hemo | homo | drugs | karnof | race | gender | symptom | treat | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10056 | 0 | 948 | 2 | 48 | 89.8128 | 0 | 0 | 0 | 100 | 0 | 0 | 0 | 1 |
| 1 | 10059 | 1 | 1002 | 3 | 61 | 49.4424 | 0 | 0 | 0 | 90 | 0 | 0 | 0 | 1 |
| 2 | 10089 | 0 | 961 | 3 | 45 | 88.4520 | 0 | 1 | 1 | 90 | 0 | 1 | 0 | 1 |
| 3 | 10093 | 0 | 1166 | 3 | 47 | 85.2768 | 0 | 1 | 0 | 100 | 0 | 1 | 0 | 1 |
| 4 | 10124 | 0 | 1090 | 0 | 43 | 66.6792 | 0 | 1 | 0 | 100 | 0 | 1 | 0 | 0 |
df.describe().round(2)
| pidnum | cid | time | trt | age | wtkg | hemo | homo | drugs | karnof | race | gender | symptom | treat | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 2139.00 | 2139.00 | 2139.00 | 2139.00 | 2139.00 | 2139.00 | 2139.00 | 2139.00 | 2139.00 | 2139.00 | 2139.00 | 2139.00 | 2139.00 | 2139.00 |
| mean | 248778.25 | 0.24 | 879.10 | 1.52 | 35.25 | 75.13 | 0.08 | 0.66 | 0.13 | 95.45 | 0.29 | 0.83 | 0.17 | 0.75 |
| std | 234237.29 | 0.43 | 292.27 | 1.13 | 8.71 | 13.26 | 0.28 | 0.47 | 0.34 | 5.90 | 0.45 | 0.38 | 0.38 | 0.43 |
| min | 10056.00 | 0.00 | 14.00 | 0.00 | 12.00 | 31.00 | 0.00 | 0.00 | 0.00 | 70.00 | 0.00 | 0.00 | 0.00 | 0.00 |
| 25% | 81446.50 | 0.00 | 727.00 | 1.00 | 29.00 | 66.68 | 0.00 | 0.00 | 0.00 | 90.00 | 0.00 | 1.00 | 0.00 | 1.00 |
| 50% | 190566.00 | 0.00 | 997.00 | 2.00 | 34.00 | 74.39 | 0.00 | 1.00 | 0.00 | 100.00 | 0.00 | 1.00 | 0.00 | 1.00 |
| 75% | 280277.00 | 0.00 | 1091.00 | 3.00 | 40.00 | 82.56 | 0.00 | 1.00 | 0.00 | 100.00 | 1.00 | 1.00 | 0.00 | 1.00 |
| max | 990077.00 | 1.00 | 1231.00 | 3.00 | 70.00 | 159.94 | 1.00 | 1.00 | 1.00 | 100.00 | 1.00 | 1.00 | 1.00 | 1.00 |
Estadísticos descriptivos (describe)¶
Análisis de lo visto hasta el momento¶
Hay 2139 registros, a continuación un análisis variable a variable excluyendo el id del paciente.
Time (Tiempo hasta fallecimiento o pérdida de registro del paciente): Se supone sobre esta variable que es tiempo en días, pues no se encontró información al respecto. En promedio pasan $879$ días hasta que el paciente fallece o se pierde registro del mismo, hay una desviación estandar significativa que representa aproximadamente un tercio del promedio. Notablemente, el primer cuartil (25%) se haya $997 - 727 = 270$ unidades a la izquierda de la media (50%), esta último se haya a $1091 - 997 = 94$ unidades a la izquierda del tercer cuartil (75%), esto sugiere una distribución con asimetría negativa. A esta misma conclusión llegamos comparando mediana y media.
Trt (Tipo de tratamiento): Al ser un categórico no resultan muy útiles los estadísticos descriptivos del promedio, desviación estándar, entre otros. Podemos asegurar que se usaron todos los tratamientos pues todos aparecen.
Age (edad): El promedio de la edad de la población estudiada es de 35.25 años. Sin embargo, los pacientes van desde los 12 hasta los 70 años. La mediana es de 34 años, lo cual, junto al promedio, puede indicar una distribución con asimetría positiva lo que denota en este caso que la población tiende a ser menor al promedio.
Wtkg (peso en kg): En promedio, los pacientes pesan 75.13 kg. El rango de pesos es, al igual que de edades, bastante amplio. Concretamente el rango de pesos es de $159.94 - 31 = 128.94$ kg. La diferencia entre la media y la mediana es de $75.13 - 74.39 = 0.74 \approx 0$. Esto sugiere una distribución centradada.
Hemo (condición hemofilia): A partir de los estadísticos descriptivos vemos que esta condición es algo rara, pues al menos el 75% no la tienen. Vemos que está en el dataset pues en la tabla el máximo es 1.
Homo (ser homosexual [binario]): El promedio de 2139 filas de esta variable binaria (con valores 0 y 1) es de $0.66$. A partir de esto podemos saber que el 66% de los pacientes con SIDA son homosexuales, esto contrasta con la proporción entre personas sanas (en nuestra experiencia), por tanto es posible que la población homosexual esté más expuesta al SIDA.
Drugs (antecedentes con drogas intravenosas): Haciendo similar análisis a la variable Homo, se estima que un 13% de la población tiene un historial de uso de drogas.
Karnof (escala Karnofsky): Los estadísticos descriptivos muestran que, en general, los pacientes tienen síntomas leves (promedio 95.45 con desviación estandar de sólo 5). El mínimo en la escala registrado es de 70 lo cuál es, según esta, equivalente a ser capaz de cuidarse por si mismo pero no de hacer alguna activdad o trabajo normal.
Race (blanco o no blanco): Igual análisis a las demás variables binarias, el 29% de los pacientes son no blancos, esto corresponde a la distribución racial estadounidense.
Gender (género hombre / mujer): EL promedio es de 0.83, como 1 representa "Hombre" para esta variable binaria, el 83% de los pacientes son hombres. Esto, junto con la prevalencia de personas homosexuales en la muestra, parece indicar ya sea un sesgo a elegir hombres homosexuales, o una predisposición de estos a contagiarse.
Symptom (síntomas clínicos al inicio): El promedio es de 0.17, 1 representa "Sintomático" para la variable binaria. Entonces, el 17% de los pacientes presentaba síntomas clínicos al inicio. Que tantos pacientes sean asintomáticos resulta interesante pues significa que sospechaban haber sido contagiados.
Treat (binario si se usa ZDV sólo o no): El promedio es de 0.75, es decir, el 75% de los pacientes son tratados utilizando solamente ZDV.
Cid (indicador de censura binario): El promedio es de 0.24, es decir, el 24% de los pacientes del estudio fallecieron. Aquí puede haber confusión sobre el significado de Cid, decidimos interpretarlo como 1 es fallecimiento derivado del propósito del dataset, de lo contrario: ¿cómo podría este servir para entrenar un modelo que prediga quienes fallecen en un intervalo dado?.
Análisis de calidad de los datos¶
¿ Qué dimensiones de calidad de datos se van a analizar?¶
Completitud: La dimensión de completitud consiste en el porcentaje de datos llenos en comparación a el total de datos.
Conformidad / Validez: Significa que los datos deben ser representados con un formato constante, adheriendose a un tipo de dato específico.
Consistencia: Los datos son congruentes entre sí, todas las relaciones existentes entre las variables son ciertas y pueden representar situaciones del mundo real, se identifican relaciones anómalas.
Precisión/ Exactitud: El dato por si solo es coherente, lógico y verdadero. Se usa para saber la diferencia entre los datos almacenados con los reales.
Duplicidad / Unicidad: Se refiere a la ocurrencia única de un objeto al ser grabado en el dataset.
Integridad: Es el grado en el que las varias fuentes de datos están sincronizadas, y que los cambios que hay en una fuente se refleje en otra.
- Completitud:¶
#Para revisar datos incompletos, sumar nulos.
df.isna().sum()
pidnum 0 cid 0 time 0 trt 0 age 0 wtkg 0 hemo 0 homo 0 drugs 0 karnof 0 race 0 gender 0 symptom 0 treat 0 dtype: int64
Se ve que todas las columnas tienen 0 datos nulos. Debido a que no hay datos faltantes, no hay que hacer un tratamiento para ellos.
Sin embargo, se considera que aunque las columnas no tienen datos incompletos, se podría completar el dataset con otras variables como la estatura, ya que puede ser útil para detectar anomalías y crear el índice de masa corporal. Este índice puede tener mayor correlación con otras variables, pero la información se pierde. Además, se podrían agregar otras variables que describan la salud del paciente, y que influyen en la progresión o mortalidad. La falta de estas hace que no se logre predecir correctamente, al ingorar variables explicativas actuales.
- Conformidad/Validez:¶
Para analizar la validez revisaremos que todas las variables realmente son del tipo que dicen ser.
Sólo hay hombres y mujeres
df[(df["time"]<=0 )| (df["wtkg"]<= 0)]
| pidnum | cid | time | trt | age | wtkg | hemo | homo | drugs | karnof | race | gender | symptom | treat |
|---|
No hay tiempo ni peso negativo
# Según el repositorio, hay 4 tratamientos posibles [0, 1, 2, 3] esto se confirma aquí.
df["trt"].value_counts().sort_index()
trt 0 532 1 522 2 524 3 561 Name: count, dtype: int64
Verificación de variables binarias
df["homo"].value_counts()
homo 1 1414 0 725 Name: count, dtype: int64
df["gender"].value_counts()
gender 1 1771 0 368 Name: count, dtype: int64
df["cid"].value_counts()
cid 0 1618 1 521 Name: count, dtype: int64
df["hemo"].value_counts()
hemo 0 1959 1 180 Name: count, dtype: int64
df["race"].value_counts()
race 0 1522 1 617 Name: count, dtype: int64
df["symptom"].value_counts()
symptom 0 1769 1 370 Name: count, dtype: int64
df["treat"].value_counts()
treat 1 1607 0 532 Name: count, dtype: int64
df["drugs"].value_counts()
drugs 0 1858 1 281 Name: count, dtype: int64
# Los valores de la Escala Karnofsky se definen en el intervalo [0, 100].
# No hay ningún dato fuera de este rango, es consistente.
df[(df["karnof"] < 0) | (df["karnof"] > 100)]
| pidnum | cid | time | trt | age | wtkg | hemo | homo | drugs | karnof | race | gender | symptom | treat |
|---|
# Se confirma el rango de edades visualmente
df[(df["age"] < 20) | (df['age'] > 60)]['age'].value_counts().sort_index()
age 12 3 13 3 14 6 15 3 16 7 17 4 18 7 19 7 61 2 62 5 63 6 64 2 65 3 66 1 67 2 68 2 69 1 70 2 Name: count, dtype: int64
El resto de las variables enteras y binarias cuentan con valores que no se salen de lo que son.
- Consistencia:¶
df[["wtkg", "age", "karnof"]].describe().T[["mean","std","min","max"]]
| mean | std | min | max | |
|---|---|---|---|---|
| wtkg | 75.125311 | 13.263164 | 31.0 | 159.93936 |
| age | 35.248247 | 8.709026 | 12.0 | 70.00000 |
| karnof | 95.446470 | 5.900985 | 70.0 | 100.00000 |
Note que el peso tiene una variación enorme, hay individuos de 31 kilos y otros de 160. Esto puede representar anomalías. A continuación valores que pueden estimar valores extremos de obesidad y desnutrición
print(df[df["wtkg"] > 120].shape[0])
print(df[ (df["wtkg"] < 40) & (df["age"]>15)].shape[0])
12 1
# Revisemos estos registros
df[df["wtkg"] > 120][["pidnum", "age", "wtkg"]]
# Se ve razonable.
| pidnum | age | wtkg | |
|---|---|---|---|
| 304 | 50629 | 46 | 123.37920 |
| 629 | 110717 | 36 | 120.65760 |
| 945 | 170972 | 38 | 130.63680 |
| 1060 | 190408 | 39 | 127.70000 |
| 1084 | 210039 | 30 | 149.00000 |
| 1177 | 220432 | 30 | 129.00000 |
| 1441 | 251059 | 45 | 127.00800 |
| 1569 | 270883 | 28 | 125.64720 |
| 1622 | 300517 | 31 | 122.47200 |
| 1707 | 320357 | 28 | 159.93936 |
| 1773 | 330242 | 30 | 135.17280 |
| 1837 | 520014 | 33 | 120.65760 |
df[(df["wtkg"] < 40) & (df["age"]>15)]
# Esta persona tiene 30 años y pesa 36.78 kg con un karnof de 90. Se ve sospechoso.
| pidnum | cid | time | trt | age | wtkg | hemo | homo | drugs | karnof | race | gender | symptom | treat | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1931 | 610043 | 0 | 225 | 2 | 30 | 36.78696 | 0 | 1 | 0 | 90 | 0 | 1 | 0 | 1 |
df[(df["wtkg"] < 40)]
| pidnum | cid | time | trt | age | wtkg | hemo | homo | drugs | karnof | race | gender | symptom | treat | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1931 | 610043 | 0 | 225 | 2 | 30 | 36.78696 | 0 | 1 | 0 | 90 | 0 | 1 | 0 | 1 |
| 2014 | 910034 | 0 | 1121 | 2 | 13 | 32.65920 | 1 | 0 | 0 | 100 | 0 | 1 | 1 | 1 |
| 2072 | 950056 | 1 | 324 | 1 | 12 | 31.00000 | 1 | 0 | 0 | 100 | 0 | 1 | 0 | 1 |
# La variable treat se puede obtener de trt, entonces, cuando trt sea 0 treat debe ser 0.
# Vemos que no hay instancias donde esto no se cumpla. La variable treat es lo que dice ser.
print(df[(df["trt"] == 0) & (df["treat"] == 0)].shape[0])
print(df[df["treat"] == 0].shape[0])
532 532
import matplotlib.pyplot as plt
print(df[['age', 'karnof']].corr(method='spearman'))
plt.scatter(df['age'], df['karnof'])
plt.xlabel('Edad')
plt.ylabel('Índice Karnofsky')
plt.show()
age karnof age 1.000000 -0.084544 karnof -0.084544 1.000000
Hay una correlacion negativa entre edad y karnofsky, el cual es esperado al debilitarse el sistema inmune y reducirse la funcionalidad física de los pacientes.
print(df[['wtkg', 'karnof']].corr(method='spearman'))
plt.scatter(df['wtkg'], df['karnof'])
plt.xlabel('Peso')
plt.ylabel('Índice Karnofsky')
plt.show()
wtkg karnof wtkg 1.000000 0.040878 karnof 0.040878 1.000000
La correlación entre peso y karnofsky puede variar, al tener menor puntaje en pesos extremos, pero valores altos en pesos saludables
import seaborn as sns
import matplotlib.pyplot as plt
vars_numericas = ["time", "age", "wtkg", "karnof"]
corr = df[vars_numericas].corr(method="pearson")
plt.figure(figsize=(5, 5))
sns.heatmap(corr, annot=True, fmt=".2f", cmap="BuPu")
plt.title("Matriz de correlación entre variables continuas")
plt.show()
Detección de outliers unidimensionales para edad, peso y coeficiente de Karnofsky¶
Decidimos detectar los outliers en estas variables ya que tienen el mayor impacto en la condición de salud del paciente.
import matplotlib.pyplot as plt
urlcsv = "http://archive.ics.uci.edu/static/public/890/data.csv"
df = pd.read_csv(urlcsv)
vars_interes = ['age', 'wtkg', 'karnof']
# Un boxplot separado para cada variable
for var in vars_interes:
plt.figure(figsize=(6,4))
df.boxplot(column=var)
plt.title(f'Boxplot de {var}')
plt.ylabel(var)
plt.grid(True)
plt.show()
Detección de outliers para edad y peso¶
Se usó un método de distancia de mahalanobis y además el DBSCAN para encontrar datos que no se acumulan. Los resultados de outliers dependen evidentemente de la distancia que tiene la curva de contorno de mahalanobis y el radio que se usa en el DBSCAN.
import pandas as pd
import numpy as np
from scipy.stats import chi2
import matplotlib.pyplot as plt
# Cargar los datos nuevamente
file_path = "http://archive.ics.uci.edu/static/public/890/data.csv"
df = pd.read_csv(file_path)
# Seleccionar variables relevantes
data = df[['age', 'wtkg']]
# Calcular media y matriz de covarianza
mean_vec = np.mean(data, axis=0)
cov_matrix = np.cov(data, rowvar=False)
inv_cov_matrix = np.linalg.inv(cov_matrix)
# Función para distancia de Mahalanobis
def mahalanobis_distance(x, mean_vec, inv_cov_matrix):
x_minus_mu = x - mean_vec
return np.sqrt(x_minus_mu @ inv_cov_matrix @ x_minus_mu.T)
# Calcular distancias
distances = data.apply(lambda row: mahalanobis_distance(row, mean_vec, inv_cov_matrix), axis=1)
# Umbral para p < 0.001
threshold = np.sqrt(chi2.ppf(0.999, df=data.shape[1]))
outliers = distances > threshold
outlier_points = data[outliers]
# Gráfico
plt.figure(figsize=(8, 6))
plt.scatter(data['age'], data['wtkg'], c='blue', label='Datos normales')
plt.scatter(outlier_points['age'], outlier_points['wtkg'], c='red', label='Outliers')
plt.xlabel('Edad')
plt.ylabel('Peso (kg)')
plt.title('Outliers (Distancia de Mahalanobis)')
plt.legend()
plt.grid(True)
plt.show()
outlier_points.head()
| age | wtkg | |
|---|---|---|
| 1 | 61 | 49.4424 |
| 49 | 67 | 71.0000 |
| 104 | 70 | 73.9368 |
| 271 | 68 | 90.5000 |
| 518 | 68 | 70.7616 |
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import DBSCAN
import matplotlib.pyplot as plt
# Cargar los datos nuevamente
file_path = "http://archive.ics.uci.edu/static/public/890/data.csv"
df = pd.read_csv(file_path)
data = df[['age', 'wtkg']]
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data)
dbscan = DBSCAN(eps = 0.6, min_samples = 10)
labels = dbscan.fit_predict(data_scaled)
data_outliers = data.copy()
data_outliers['cluster'] = labels
outliers_detectados = data_outliers[data_outliers['cluster'] == -1]
print(f"Número de outliers detectados: {outliers_detectados.shape[0]}")
labels = data_outliers['cluster'].values
unique_labels = np.unique(labels)
plt.figure(figsize=(8,6))
for lab in unique_labels:
idx = labels == lab
if lab == -1:
# Outliers (DBSCAN noise)
plt.scatter(data_outliers.loc[idx, 'age'],
data_outliers.loc[idx, 'wtkg'],
c='black', marker='x', label='Outliers')
else:
plt.scatter(data_outliers.loc[idx, 'age'],
data_outliers.loc[idx, 'wtkg'],
label=f'Cluster {lab}')
plt.title("Clusters y outliers detectados por DBSCAN (Age vs Weight)")
plt.xlabel("Edad (age)")
plt.ylabel("Peso (wtkg)")
plt.legend()
plt.show()
Número de outliers detectados: 21
- Precisión/Exactitud:¶
Para la precisión se analizará si las variables están en rangos normales, especificamente para el peso y la edad.
df[(df["wtkg"] > 200 ) | (df["wtkg"] < 20)]
| pidnum | cid | time | trt | age | wtkg | hemo | homo | drugs | karnof | ... | gender | str2 | strat | symptom | treat | offtrt | cd40 | cd420 | cd80 | cd820 |
|---|
0 rows × 25 columns
df[(df["age"] > 100) | (df["age"]<0)]
| pidnum | cid | time | trt | age | wtkg | hemo | homo | drugs | karnof | ... | gender | str2 | strat | symptom | treat | offtrt | cd40 | cd420 | cd80 | cd820 |
|---|
0 rows × 25 columns
Hay fallas en la precisión en la variable race, al representar de manera precisa únicamente los individuos blancos. Aquellos registros con valor de race = 1, no se les puede representar de manera precisa al únicamente poder decir que el individuo no es blanco. Esto puede tener relevancia, ya que se pierde información en la correlación entre la raza y otras variables.
- Duplicidad/Unicidad:¶
df.duplicated().sum()
np.int64(0)
No existen registros repetidos
# Cada paciente tiene un id único.
df['pidnum'].is_unique
True
- Integridad:¶
Todos los datos provienen de una misma fuente, al sólo haber una fuente de datos es evidente que debe estar sincronizada consigo misma. Es decir, los datos cumplen con la dimensión de calidad "integridad".
Pasos futuros a desarrollar¶
Antes de continuar de pasar a otros pasos del CRISP-DM, se debería de revisar la posibilidad de usar datos mas recientes, esoecialmente que cumplan con las dimensiones de completitud y precisión según con lo escrito anteriormente. Además, se debe preparar los datos, especialmente crear nuevas variables más descriptivas para poder predecir la mortalidad. Se debe partir el dataset para entrenamiento, testeo y validación.
Después de esto, se debe proseguir con el modelado, en el cual se debe de escoger un modelo de clasificación.
Para la evaluación se debe evaluar el desempeño del modelo con algunos estadísticos y medidas de error. También, se debe ver si el modelo cumple los objetivos de negocio.
En caso tal, se puede proceder con el despliegue, el cual incluye la documentación de resultados y subir el modelo, ya sea a un servidor, a un servicio cloud o a un contenedor, etc.
df2 = pd.read_csv("CountyHealthData_2014-2015.csv")
Punto 2¶
#a)
dfg = df2.groupby('State')['Adult smoking'].mean().sort_values(ascending=False)
print(dfg.head(5))
#b)
import sqlite3
import sqlalchemy as sqla
db = sqla.create_engine('sqlite:///datos.sqlite')
df2.to_sql('datos', con=db, if_exists='append', index=False)
tabla = pd.read_sql("SELECT * FROM datos", db)
query = """
SELECT State, AVG("Adult smoking") AS "Smoking rate" FROM datos GROUP BY State ORDER BY "Smoking rate" DESC LIMIT 5;"""
resultado = pd.read_sql(query, db)
resultado
State KY 0.285237 AK 0.272043 WV 0.267182 TN 0.259626 MO 0.254155 Name: Adult smoking, dtype: float64
| State | Smoking rate | |
|---|---|---|
| 0 | KY | 0.285237 |
| 1 | AK | 0.272043 |
| 2 | WV | 0.267182 |
| 3 | TN | 0.259626 |
| 4 | MO | 0.254155 |
import pandas as pd
import numpy as np
import matplotlib
df2 = pd.read_csv("CountyHealthData_2014-2015.csv")
df2
| State | Region | Division | County | FIPS | GEOID | SMS Region | Year | Premature death | Poor or fair health | ... | Drug poisoning deaths | Uninsured adults | Uninsured children | Health care costs | Could not see doctor due to cost | Other primary care providers | Median household income | Children eligible for free lunch | Homicide rate | Inadequate social support | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | AK | West | Pacific | Aleutians West Census Area | 2016 | 2016 | Insuff Data | 1/1/2014 | NaN | 0.122 | ... | NaN | 0.374 | 0.250 | 3791.0 | 0.185 | 216.0 | 69192 | 0.127 | NaN | 0.287 |
| 1 | AK | West | Pacific | Aleutians West Census Area | 2016 | 2016 | Insuff Data | 1/1/2015 | NaN | 0.122 | ... | NaN | 0.314 | 0.176 | 4837.0 | 0.185 | 254.0 | 74088 | 0.133 | NaN | NaN |
| 2 | AK | West | Pacific | Anchorage Borough | 2020 | 2020 | Region 22 | 1/1/2014 | 6827.0 | 0.125 | ... | 15.37 | 0.218 | 0.096 | 6588.0 | 0.119 | 135.0 | 71094 | 0.319 | 6.29 | 0.160 |
| 3 | AK | West | Pacific | Anchorage Borough | 2020 | 2020 | Region 22 | 1/1/2015 | 6856.0 | 0.125 | ... | 17.08 | 0.227 | 0.123 | 6582.0 | 0.119 | 148.0 | 76362 | 0.334 | 5.60 | NaN |
| 4 | AK | West | Pacific | Bethel Census Area | 2050 | 2050 | Insuff Data | 1/1/2014 | 13345.0 | 0.211 | ... | NaN | 0.394 | 0.124 | 5860.0 | 0.200 | 169.0 | 41722 | 0.668 | 12.77 | 0.477 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 6104 | WY | West | Mountain | Uinta County | 56041 | 56041 | Insuff Data | 1/1/2015 | 7436.0 | 0.135 | ... | 18.66 | 0.192 | 0.090 | 7600.0 | 0.123 | 47.0 | 60953 | 0.273 | NaN | NaN |
| 6105 | WY | West | Mountain | Washakie County | 56043 | 56043 | Insuff Data | 1/1/2014 | 6580.0 | 0.106 | ... | NaN | 0.225 | 0.086 | 8202.0 | 0.099 | 47.0 | 49533 | 0.328 | NaN | 0.133 |
| 6106 | WY | West | Mountain | Washakie County | 56043 | 56043 | Insuff Data | 1/1/2015 | 7572.0 | 0.106 | ... | NaN | 0.226 | 0.101 | 7940.0 | 0.099 | 47.0 | 50740 | 0.309 | NaN | NaN |
| 6107 | WY | West | Mountain | Weston County | 56045 | 56045 | Insuff Data | 1/1/2014 | 5633.0 | 0.162 | ... | NaN | 0.201 | 0.084 | 6906.0 | 0.130 | 28.0 | 53665 | 0.232 | NaN | 0.171 |
| 6108 | WY | West | Mountain | Weston County | 56045 | 56045 | Insuff Data | 1/1/2015 | 7819.0 | 0.162 | ... | NaN | 0.189 | 0.092 | 7238.0 | 0.130 | 28.0 | 59314 | 0.235 | NaN | NaN |
6109 rows × 64 columns
Condados con FEI mayores a 9 (Usando filtrado de Pandas)¶
dfFEI9 = df2[df2["Food environment index"]>=9]
dfFEI9u = dfFEI9.drop_duplicates(['State','County'])
dfFEI9["State"].value_counts().head(5)
State VA 64 ND 33 MN 31 NY 19 MD 18 Name: count, dtype: int64
Condados con FEI mayores a 9 (Usando SQLite)¶
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine('sqlite:///countyhealth.db', echo=False)
county_health = df2.to_sql('county_health', con = engine, index = False, if_exists = 'replace')
query = """
SELECT State, County, [Food environment index]
FROM county_health
WHERE [Food environment index] >= 9
"""
df2_filtrado = pd.read_sql(query, con = engine)
top5_estados = (df2_filtrado.groupby('State')
.size()
.sort_values(ascending = False)
.head(5)
)
print(df2_filtrado.head())
print(top5_estados)
State County Food environment index 0 CO Douglas County 9.111 1 CT Litchfield County 9.020 2 GA Forsyth County 9.119 3 GA Forsyth County 9.000 4 GA Harris County 9.117 State VA 64 ND 33 MN 31 NY 19 MD 18 dtype: int64
1.3¶
nuevo = df[["State","County", "Dentists", "Primary care physicians", "2011 population estimate"]]
elegidos = nuevo[nuevo["Dentists"] > 5*nuevo["Primary care physicians"]]
sind = elegidos.groupby(["State", "County"])[["Dentists", "Primary care physicians", "2011 population estimate"]].agg({
"Dentists": "mean",
"Primary care physicians": "mean",
"2011 population estimate": "mean"}).reset_index()
sind["per capita"] = nuevo["Dentists"] / nuevo["2011 population estimate"]
nuevo = sind.sort_values(by="Dentists", ascending = False)
percapit = sind.sort_values(by = "per capita", ascending = False).head(3)
print("Counties en que hay mas de 5 veces la cantidad de dentistas, de las que hay de primary care physicians, ordenado por mayor cantidad de dentistas")
nuevo[["County", "Dentists", "Primary care physicians"]]
print("Counties con mayor dentistas per capita")
percapit[["County", "Dentists", "Primary care physicians", "per capita"]]
Counties en que hay mas de 5 veces la cantidad de dentistas, de las que hay de primary care physicians, ordenado por mayor cantidad de dentistas Counties con mayor dentistas per capita
| County | Dentists | Primary care physicians | per capita | |
|---|---|---|---|---|
| 7 | Garden County | 53.0 | 0.0 | 0.027944 |
| 6 | Deuel County | 103.0 | 0.0 | 0.027414 |
| 1 | Dolores County | 49.0 | 0.0 | 0.013246 |
¿Qué estados tienen más de 100 condados? (Pandas)¶
import plotly.express as px
df2_u = df2.drop_duplicates("FIPS")
condados_por_estado = df2_u.groupby('State')['FIPS'].count().reset_index(name = 'num_condados')
estados_mas_de_100 = condados_por_estado[condados_por_estado['num_condados'] >= 100]
estados_mas_de_100
fig = px.treemap(estados_mas_de_100,
path=['State'],
values='num_condados',
title='Estados con más de 100 condados únicos')
fig.show()
¿Qué estados tienen más de 100 condados? (SQLite)¶
!pip install squarify
Requirement already satisfied: squarify in c:\users\jtjua\anaconda3\lib\site-packages (0.4.4)
import pandas as pd
from sqlalchemy import create_engine, MetaData, Table, select, func
import matplotlib.pyplot as plt
import squarify
# 1. Leer CSV
df = pd.read_csv("CountyHealthData_2014-2015.csv")
# 2. Guardarlo en SQLite en una tabla nueva
engine = create_engine("sqlite:///healthdata.db")
df.to_sql("county_health_new", engine, if_exists="replace", index=False)
# 3. Reflejar la tabla
metadata = MetaData()
metadata.reflect(bind=engine)
county_health = Table("county_health_new", metadata, autoload_with=engine)
# 4. Query: contar condados únicos por estado y filtrar los >100
stmt = (
select(
county_health.c.State,
func.count(func.distinct(county_health.c.FIPS)).label("num_condados")
)
.group_by(county_health.c.State)
.having(func.count(func.distinct(county_health.c.FIPS)) >= 100)
.order_by(func.count(func.distinct(county_health.c.FIPS)).desc())
)
with engine.connect() as conn:
result = conn.execute(stmt).fetchall()
for row in result:
print(row)
# Convertir el resultado en DataFrame
result_df = pd.DataFrame(result, columns=['State', 'num_condados'])
# Graficar treemap
plt.figure(figsize=(12,8))
squarify.plot(sizes=result_df['num_condados'],
label=result_df['State'],
alpha=0.8)
plt.title('Número de condados (>= 100) por estado', fontsize=16)
plt.axis('off')
plt.show()
('TX', 237)
('GA', 159)
('VA', 133)
('KY', 120)
('MO', 115)
('IL', 102)
('KS', 101)
('NC', 100)
Punto 4.¶
a) Aunque la noticia tiene información verídica, es presentada de forma engañosa. Además, no cumple con algunos ítems de la estsética de visualización de Tufte
El área de los triángulos no cambia con respecto a la cantidad que se quierer representar, esto ocurre porque solo se tuvo en cuenta la altura del triángulo al momento de realizar la gráfica
La gráfica se presenta con distintos colores innecesariamente
Existen elementos que sobran alrededor de la gráfica
https://www.centrodememoriahistorica.gov.co/micrositios/informeGeneral/estadisticas.html
import pandas as pd
# Con lista de listas
tipo = ['Grupos paramilitares', 'Grupos armados no identificados', 'Guerrillas', 'Fuerza pública']
valor = [8903,6406, 3899, 2399 ]
df_lista = pd.DataFrame({ "tipo": tipo, "valor" : valor} )
print("DataFrame desde lista de listas:")
df_lista
DataFrame desde lista de listas:
| tipo | valor | |
|---|---|---|
| 0 | Grupos paramilitares | 8903 |
| 1 | Grupos armados no identificados | 6406 |
| 2 | Guerrillas | 3899 |
| 3 | Fuerza pública | 2399 |
import matplotlib.pyplot as plt
figura = plt.bar(df_lista["tipo"], df_lista["valor"], color = '0.55', width = 0.5 )
for bar in figura:
altura = bar.get_height()
plt.text( bar.get_x() + bar.get_width() / 2, altura + 15,
f'{int(altura):,}',
ha='center', va='bottom', fontsize=9) #Valor en Y
ax = plt.gca() # Obtener los ejes actuales
for spine in ['top', 'right', 'left']:
ax.spines[spine].set_visible(False)
ax.tick_params(axis='y', left=False, labelleft=True)
plt.ylabel("Número de victimas", fontsize=10) #Titulo y
plt.xticks(fontsize = 6) #Nombres x
plt.title("Asesinatos selectivos", fontweight = "bold", fontsize = 12)
plt.show()
- El tipo de gráfica presentado es adecuado para el tipo de datos que se quieren mostrar
- Se maximiza la relación entre tinta usada en datos, y tinta total
- Se usan escalas adecuadas y etiquetas limpias
- Se reduce la cantidad de basura en la gráfica
- No se distorsiona información con escalas
- La información de la gráfica es clara